﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.ServiceProcess;
using System.Text;
using System.Timers;
using System.Threading;

namespace RamDiskSupporter
{
    /**
     * RamDiskSupporterService: Windows起動/終了時に、RamDiskの初期化とバックアップを支援するサービスクラス。
     * @Auther: Kazutaka Takarabe
     * @CreateDate: 2012-03-04
     * @Copyright: IT-SOF-KT
     */
    public partial class RamDiskSupporterService : ServiceBase
    {
        // 設定情報を格納するレジストリキー
        const string registryKey = @"SYSTEM\CurrentControlSet\services\RamDiskSupporter";

        // イベント設定情報が格納される設定ファイルパス情報
        static public string settingFileName = @"C:\ReadableFiles\RamDiskSupporterSetting.xml";

        // 出力するログファイルパス情報
        static public string logFileName = @"C:\ReadableFiles\RamDiskSupporter.log";

        // このサービスが開始されているかを判定するためのフラグ
        static public bool doStartUp = false;

        // サービスが実行する処理を格納する設定情報
        static public Setting setting = new Setting();

        // サービスが実行した処理の統計情報を格納するためのカウンター
        static public Counter counter = new Counter();

        // スレッドセーフを行うためのロックオブジェクト
        static private readonly object lockObj = new object();

        // サービスプロセスが開始された時のOnStartイベントを実行するスレッド
        private System.Timers.Timer timer = new System.Timers.Timer();

        // ディレクトリ同期処理を実行するためのFileSystemWatcherを格納するリスト
        private List<FileSystemWatcher> watcherContainer = new List<FileSystemWatcher>();

        // FileSystemWatcherのインスタンスを実行するためのスレッドを格納するリスト
        private List<Thread> watcherThreadContainer = new List<Thread>();

        // このサービスのコンストラクタ
        public RamDiskSupporterService()
        {
            InitializeComponent();

            // レジストリから起動情報を取得
            try
            {
                Microsoft.Win32.RegistryKey rKey = Microsoft.Win32.Registry.LocalMachine.CreateSubKey(registryKey);
                settingFileName = rKey.GetValue("SettingFilePath").ToString();
                logFileName = rKey.GetValue("LogFilePath").ToString();
                rKey.Close();
            }
            catch (Exception _e)
            {
                throw new Exception("Access denied for registries.", _e);
            }

            // 主処理を実行するスレッドタイマーの起動準備
            timer.Elapsed += new ElapsedEventHandler(TimerElapsed);
            timer.Interval  = 100;
            timer.AutoReset = true;
        }


        /**
         * TimerElapsed: サービス実行中に別スレッドからコールされるハンドラ
         */
        private void TimerElapsed(object sender, System.Timers.ElapsedEventArgs elapsedEventArgs)
        {
            // 多重実行されないためにスレッドをロックする。
            lock (lockObj)
            {
                // 既に実行済みの場合は処理しない。
                if (doStartUp) return;

                // 開始処理を実行する。
                const string a = "OnStart";
                foreach (Event e in setting.OnStartEvents)
                {
                    // ディレクトリ作成イベント
                    if (e._Type.Equals("CREATE")) OnCreateEvent(a, e);

                    // ディレクトリ削除イベント
                    if (e._Type.Equals("DELETE"))
                    {
                        OnDeleteEvent(a, e);

                        // リアルタイム削除オプション
                        if (e._Sync)
                        {
                            try
                            {
                                watcherContainer.Add(getFileSystemWatcher(e._Type, e._Filter, e._Source, e._Target));
                                WriteLog("Start observing directory process.\r\nThese directories are automatically deleted.\r\n\r\n" + e._Source, EventLogEntryType.Information);
                            }
                            catch (Exception _E)
                            {
                                WriteLog("Start-up failed observing directory process.\r\n" + _E.Message + "\r\n\r\n" + e._Source+ "\r\n\r\n" + _E.StackTrace, EventLogEntryType.Information);
                            }
                        }
                    }

                    // ディレクトリ複製イベント
                    if (e._Type.Equals("COPY"))
                    {
                        OnCopyEvent(a, e);

                        // リアルタイム同期オプション
                        if (e._Sync)
                        {
                            try
                            {
                                watcherContainer.Add(getFileSystemWatcher(e._Type, e._Filter, e._Source, e._Target));
                                watcherContainer.Add(getFileSystemWatcher(e._Type, e._Filter, e._Target, e._Source));
                                WriteLog("Start synchronizasion directory process.\r\nThese directories are automatically synchronized.\r\n\r\n" + e._Source + " <> " + e._Target, EventLogEntryType.Information);
                            }
                            catch (Exception _E)
                            {
                                WriteLog("Start-up failed synchronizasion directory process.\r\n"+_E.Message+"\r\n\r\n" + e._Source + " <> " + e._Target +"\r\n\r\n"+_E.StackTrace, EventLogEntryType.Information);
                            }
                        }
                    }
                }

                // 稼働中フラグを有効にする。
                doStartUp = true;
            }
        }

        /**
         * OnStart: Serviceが開始する時に呼び出されるイベントハンドラ。
         */
        protected override void OnStart(string[] args)
        {
            // 開始処理を実行するために設定ファイルを読み込む。
            if (LoadConfiguration())
            {
                // サービス開始処理スレッドをスタートする。
                timer.Enabled = true;
            }
        }

        /**
         * OnStop: Serviceが停止する時に呼び出されるイベントハンドラ。
         */
        protected override void OnStop()
        {
            // サービス開始処理スレッドが停止していない場合はスレッドを停止する。
            if (timer.Enabled) timer.Enabled = false;

            // ファイルシステム監視を停止する。
            foreach (FileSystemWatcher fsw in watcherContainer)
            {
                fsw.EnableRaisingEvents = false;
                fsw.Dispose();
            }

            // サービスの停止フラグを設定する。
            doStartUp = false;

            // 終了処理を実行するために設定ファイルを再読み込みする。
            if (LoadConfiguration())
            {
                // 終了処理を実行する。
                const string a = "OnStop";
                foreach (Event e in setting.OnStopEvents)
                {
                    if (e._Type.Equals("CREATE")) OnCreateEvent(a, e);
                    if (e._Type.Equals("COPY")) OnCopyEvent(a, e);
                    if (e._Type.Equals("DELETE")) OnDeleteEvent(a, e);
                }
            }
        }

        /**
         * OnShutdown: WindowsがShutdownする時に呼び出されるイベントハンドラ。
         */
        protected override void OnShutdown()
        {
            this.OnStop();
        }

        /**
         * OnCreateEvent: ディレクトリ作成イベントを処理する。
         * @param String a 現在のサービスプロセス状態(OnStart or OnStop)
         * @param Event e 処理を行うイベント構造体
         */
        private Event OnCreateEvent(string a, Event e)
        {
            counter = new Counter();
            CreateDirectory(e._Target);

            if (counter._tError.Count == 0)
            {
                WriteLog("Creating directory successfully completed.\r\n" +
                    toStringEvent(e, a) + toStringCounter(counter, a), EventLogEntryType.Information);
            }
            else
            {
                WriteLog("Creating directory failed.\r\n" +
                    toStringEvent(e, a) + toStringCounter(counter, a), EventLogEntryType.Warning);
            }
            return e;
        }

        /**
         * OnCopyEvent: ディレクトリコピーイベントを処理する。
         * @param String a 現在のサービスプロセス状態(OnStart or OnStop)
         * @param Event e 処理を行うイベント構造体
         */
        private Event OnCopyEvent(string a, Event e)
        {
            counter = new Counter();
            CopyDirectory(e._Source, e._Target, e._Override, e._Sync);

            if (counter._tError.Count == 0)
            {
                WriteLog("Copying directory successfully completed.\r\n" +
                    toStringEvent(e, a) + toStringCounter(counter, a), EventLogEntryType.Information);
            }
            else
            {
                WriteLog("Copying directory failed.\r\n" +
                    toStringEvent(e, a) + toStringCounter(counter, a), EventLogEntryType.Warning);
            }
            return e;
        }

        /**
         * OnDeleteEvent: ディレクトリ削除イベントを処理する。
         * @param String a 現在のサービスプロセス状態(OnStart or OnStop)
         * @param Event e 処理を行うイベント構造体
         */
        private Event OnDeleteEvent(string a, Event e)
        {
            counter = new Counter();
            DeleteDirectory(e._Target);

            if (counter._tError.Count == 0)
            {
                WriteLog("Deleting directory successfully completed.\r\n" +
                    toStringEvent(e, a) + toStringCounter(counter, a), EventLogEntryType.Information);
            }
            else
            {
                WriteLog("Deleting directory failed.\r\n" + 
                    toStringEvent(e, a) + toStringCounter(counter, a), EventLogEntryType.Warning);
            }
            return e;
        }

        /*
         * CreateDirectory: 指定されたディレクトリが存在するか確認し、存在しない場合はディレクトリを作成する。
         * @param String stTargetPath
         */
        private void CreateDirectory(string stTargetPath)
        {
            // 指定されたディレクトリが存在しない場合は、ディレクトリを作成する。
            try
            {
                counter._tDir++;
                if (!Directory.Exists(stTargetPath))
                {
                    Directory.CreateDirectory(stTargetPath);
                    File.SetAttributes(stTargetPath, File.GetAttributes(stTargetPath));
                    counter._Dir++;
                }
            }
            // ディレクトリが作成できない場合は、エラーを記録して終了。
            catch (Exception _e) { counter._tError.Add(_e.Message); return; }
        }

        /*
         * CopyDirectory: 指定されたディレクトリ配下に存在するファイル・サブディレクトリを、指定された対象ディレクトリにコピーする。
         * @param String stSourcePath
         * @param String stTargetPath
         * @param Boolean bOverwrite
         */
        private void CopyDirectory(string stSourcePath, string stTargetPath, bool bOverwrite, bool bSync)
        {
            // 指定されたディレクトリが存在しない場合は、ディレクトリを作成して上書きモードで処理実行。
            bool isCreatableDir = false;
            try
            {
                counter._tDir++;
                if (!Directory.Exists(stTargetPath))
                {
                    Directory.CreateDirectory(stTargetPath);
                    File.SetAttributes(stTargetPath, File.GetAttributes(stTargetPath));
                    bOverwrite = true;
                    counter._Dir++;
                    isCreatableDir = true;
                }
            }
            // ディレクトリが作成できない例外時は、以下のファイルコピーは不可能なので処理を中断する。
            catch (Exception _e) { counter._tError.Add(_e.Message); return; }


            // 指定されたディレクトリ配下のファイルを処理モードに従ってコピーする。
            foreach (string stCopyFrom in Directory.GetFiles(stSourcePath))
            {
                string stCopyTo = Path.Combine(stTargetPath, Path.GetFileName(stCopyFrom));
                counter._tFile++;

                // 上書きモードの場合、コピー元のディレクトリにあるすべてのファイルをコピーする
                // 上書きモードでない場合、ファイルが存在しないか、タイムスタンプが異なる場合のみコピーする
                if (bOverwrite || !File.Exists(stCopyTo) || (File.GetLastWriteTime(stCopyFrom) != File.GetLastWriteTime(stCopyTo)))
                {
                    try
                    {
                        File.Copy(stCopyFrom, stCopyTo, true);
                        counter._File++;
                    }
                    catch (Exception _e) { counter._tError.Add(_e.Message); }
                }
            }

            // 同期モードが有効な場合は、コピー先にある不要なファイル（コピー元で削除されたファイル）を削除する。ただし作成されたディレクトリの場合は必要ない。
            if (bSync && !isCreatableDir) DeleteNotExistFiles(stSourcePath, stTargetPath);


            // 指定されたディレクトリ配下のサブディレクトリを再起処理する。
            foreach (string stCopyFrom in Directory.GetDirectories(stSourcePath))
            {
                string stCopyTo = Path.Combine(stTargetPath, Path.GetFileName(stCopyFrom));
                CopyDirectory(stCopyFrom, stCopyTo, bOverwrite, bSync);
            }
        }

        /*
         * DeleteNotExistFiles: コピー先に存在するがコピー元に存在しないファイルとディレクトリを削除する。（要はコピー先のゴミ掃除）
         * @param string stSourcePath
         * @param string stTargetPath
         */
        private void DeleteNotExistFiles(string stSourcePath, string stTargetPath)
        {
            //コピー先に存在するがコピー元には存在しないファイルを削除する。
            foreach (string f in Directory.GetFiles(stTargetPath))
            {
                string fName = Path.Combine(stSourcePath, Path.GetFileName(f));
                if (!File.Exists(fName))
                {
                    File.Delete(f);
                    counter._tMessage.Add("Delete not exist file. " + f);
                }
            }

            //コピー先に存在するがコピー元には存在しないディレクトリを削除する。
            foreach (string f in Directory.GetDirectories(stTargetPath))
            {
                string fName = Path.Combine(stSourcePath, Path.GetFileName(f));
                if (!Directory.Exists(fName))
                {
                    Directory.Delete(f, true);
                    counter._tMessage.Add("Delete not exist directory. " + f);
                }
            }
        }

        /*
         * DeleteDirectory: 指定されたディレクトリ配下に存在するファイル・サブディレクトリを削除する。
         * @param String stTargetPath
         */
        private void DeleteDirectory(string stTargetPath)
        {
            // 指定されたディレクトリが存在しない場合は処理成功。
            if (!Directory.Exists(stTargetPath)) return;

            // 指定されたディレクトリ配下のファイルを削除する。
            foreach (string fName in Directory.GetFiles(stTargetPath))
            {
                string stDeleteTo = Path.Combine(stTargetPath, Path.GetFileName(fName));
                counter._tFile++;

                try
                {
                    File.Delete(stDeleteTo);
                    counter._File++;
                }
                catch (Exception _e) { counter._tError.Add(_e.Message); }
            }

            // 指定されたディレクトリ配下のサブディレクトリを再起処理し、配下のファイルを削除した時点でサブディレクトリも削除する。
            foreach (string fName in Directory.GetDirectories(stTargetPath))
            {
                string stDeleteTo = Path.Combine(stTargetPath, Path.GetFileName(fName));
                counter._tDir++;
                DeleteDirectory(stDeleteTo);

                try
                {
                    Directory.Delete(stDeleteTo, true);
                    counter._Dir++;
                }
                catch (Exception _e) { counter._tError.Add(_e.Message); }
            }
        }

        /*
         * サービスが処理するイベント設定ファイルをロードする。
         */
        private bool LoadConfiguration()
        {
            System.IO.FileStream fs = null;

            try
            {
                fs = new System.IO.FileStream(settingFileName, System.IO.FileMode.Open);
                System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(typeof(Setting));
                setting = (Setting)serializer.Deserialize(fs);
                fs.Close();
                fs = null;
            }
            catch (Exception _e)
            {
                if (fs != null) fs.Close();
                WriteLog("Loading setting failed." + "\r\n" + _e.Message + "\r\n" + _e.StackTrace, EventLogEntryType.Error);
                throw;
            }
            return true;
        }

        /*
         * ファイル同期サービスを監視するためのウォッチャーをビルドして返す。
         */
        private FileSystemWatcher getFileSystemWatcher(string type, string[] filter, string observePath, string targetPath)
        {
            //ファイルシステム監視の設定を行う。
            FileSystemWatcher watcher = new System.IO.FileSystemWatcher(observePath);
            watcher.NotifyFilter = (NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName);
            watcher.Filter = "*.*";
            watcher.IncludeSubdirectories = true;
            watcher.InternalBufferSize = 64 * 1024;

            //イベントハンドラを設定する。
            OnSyncEvent syncFiles = new OnSyncEvent(type, observePath, targetPath);
            watcher.Created += new System.IO.FileSystemEventHandler(syncFiles.Created);
            watcher.Changed += new System.IO.FileSystemEventHandler(syncFiles.Changed);
            watcher.Deleted += new System.IO.FileSystemEventHandler(syncFiles.Deleted);
            watcher.Renamed += new System.IO.RenamedEventHandler(syncFiles.Renamed);

            //監視を開始する。
            watcher.EnableRaisingEvents = true;
            return watcher;
        }

        /*
         * 指定されたEvent構造体をログに変換して返す。
         * @param Event e
         * @param String process
         * @return String
         */
        private string toStringEvent(Event e, string process)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("\r\n");
            sb.Append("Process: ").Append(process).Append("\r\n");
            sb.Append("Type: ").Append(e._Type).Append("\r\n");
            sb.Append("Source: ").Append(e._Source).Append("\r\n");
            sb.Append("Target: ").Append(e._Target).Append("\r\n");
            sb.Append("Override: ").Append(e._Override).Append("\r\n");
            return sb.ToString();
        }

        /*
         * 指定されたCounterをログに整形して返す。
         * @param Counter c
         * @param String process
         * @return String
         */
        private string toStringCounter(Counter c, string process)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("\r\n");
            sb.Append("Directories: ").Append(c._Dir).Append("/").Append(c._tDir).Append("\r\n");
            sb.Append("Files: ").Append(c._File).Append("/").Append(c._tFile).Append("\r\n");
            sb.Append("\r\n");

            if (c._tMessage.Count > 0)
            {
                sb.Append("Messages: ").Append(c._tMessage.Count).Append("\r\n");
                int i = 0;
                foreach (string log in c._tMessage)
                {
                    i++;
                    sb.Append(i).Append(". ").Append(log).Append("\r\n");
                }
                sb.Append("\r\n");
            }

            if (c._tError.Count > 0)
            {
                sb.Append("ErrorCount: ").Append(c._tError.Count).Append("\r\n");
                int i = 0;
                foreach (string log in c._tError)
                {
                    i++;
                    sb.Append(i).Append(". ").Append(log).Append("\r\n");
                }
                sb.Append("\r\n");
            }
            return sb.ToString();
        }

        /*
         * 指定された情報に基づいてEventLogを出力する。
         * @param string a 
         * @param EventLogEntryType e
         * 
         */
        private void WriteLog(string a, EventLogEntryType e)
        {
            try
            {
                EventLog.WriteEntry(a, e);

                string curretlogFileName = logFileName.Replace(".log", DateTime.Now.ToString("_yyyyMMdd") + ".log");
                using (StreamWriter sw = new StreamWriter(curretlogFileName, true))
                {
                    sw.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fffff") +" - "+ a);
                }
            }
            catch { }
        }
    }

    /**
     * Class: SynchronizeFiles
     */
    public class OnSyncEvent
    {
        private string type = null;
        private string sourcePath = null;
        private string targetPath = null;

        public OnSyncEvent(string eventType, string sourcePath, string targetPath)
        {
            this.type = eventType;
            this.sourcePath = sourcePath;
            this.targetPath = targetPath;
        }

        public void Created(System.Object source, System.IO.FileSystemEventArgs e)
        {
            // サービスの開始/終了処理中は多重実行となるため処理しない。
            if (!RamDiskSupporterService.doStartUp) return;

            // 更新されたのがディレクトリかファイルかを判別する。
            bool isDirectory = System.IO.Directory.Exists(e.FullPath);
            bool isFile = System.IO.File.Exists(e.FullPath);

            // 更新ファイルの対となるファイル名を取得する。
            string targetFilePath = e.FullPath.Replace(sourcePath, targetPath);

            // 対象に存在しない場合、サイズ・更新日時等の属性を比較して異なる場合はコピーする。
            EventLog.WriteEntry("RamDiskSuppoter", e.FullPath + " is created. i'll copy file to " + targetFilePath, EventLogEntryType.Information);
        }

        public void Changed(System.Object source, System.IO.FileSystemEventArgs e)
        {
            // サービスの開始/終了処理中は多重実行となるため処理しない。
            if (!RamDiskSupporterService.doStartUp) return;

            // 更新されたのがディレクトリかファイルかを判別する。
            bool isDirectory = System.IO.Directory.Exists(e.FullPath);
            bool isFile = System.IO.File.Exists(e.FullPath);

            // 更新ファイルの対となるファイル名を取得する。
            string targetFilePath = e.FullPath.Replace(sourcePath, targetPath);

            // 対象に存在しない場合、サイズ・更新日時等の属性を比較して異なる場合はコピーする。
            EventLog.WriteEntry("RamDiskSuppoter", e.FullPath + " is changed. i'll copy file to " + targetFilePath, EventLogEntryType.Information);
        }

        public void Deleted(System.Object source, System.IO.FileSystemEventArgs e)
        {
            // サービスの開始/終了処理中は多重実行となるため処理しない。
            if (!RamDiskSupporterService.doStartUp) return;

            // 更新されたのがディレクトリかファイルかを判別する。
            bool isDirectory = System.IO.Directory.Exists(e.FullPath);
            bool isFile = System.IO.File.Exists(e.FullPath);

            // 更新ファイルの対となるファイル名を取得する。
            string targetFilePath = e.FullPath.Replace(sourcePath, targetPath);

            // 対象に存在しない場合、サイズ・更新日時等の属性を比較して異なる場合はコピーする。
            EventLog.WriteEntry("RamDiskSuppoter", e.FullPath + " is deleted. i'll delete file to " + targetFilePath, EventLogEntryType.Information);
        }

        public void Renamed(System.Object source, System.IO.RenamedEventArgs e)
        {
            // サービスの開始/終了処理中は多重実行となるため処理しない。
            if (!RamDiskSupporterService.doStartUp) return;

            // 更新されたのがディレクトリかファイルかを判別する。
            bool isDirectory = System.IO.Directory.Exists(e.OldFullPath);
            bool isFile = System.IO.File.Exists(e.OldFullPath);

            // 更新ファイルの対となるファイル名を取得する。
            string oldTargetFilePath = e.OldFullPath.Replace(sourcePath, targetPath);
            string newTargetFilePath = e.FullPath.Replace(sourcePath, targetPath);

            // 変更前の名前で対象が存在する場合は名前を変更する。
            EventLog.WriteEntry("RamDiskSuppoter", e.OldFullPath + " (" + e.FullPath + ") is renamed. i'll change name file to " + oldTargetFilePath + " to " + newTargetFilePath, EventLogEntryType.Information);
        }
    }

    /**
     * Class: Setting
     */
    public class Setting
    {
        public List<Event> OnStartEvents = new List<Event>();
        public List<Event> OnStopEvents = new List<Event>();
    }

    /**
     * Struct: Event
     */
    public struct Event
    {
        public string _Type;        //- イベント種別(OnStart|OnStop|OnSync)
        public string _Source;      //- コピー元パス
        public string _Target;      //- コピー先パス
        public bool _Override;      //- 全て上書きするか
        public bool _Sync;          //- 同期処理するか(コピー先にあるゴミを削除するか)
        public string[] _Filter;     //- 削除対象とするフィルター定義
    }

    /**
     * Class: Counter
     */
    public class Counter
    {
        public Counter()
        {
            this._tMessage = new List<string>();
            this._tError = new List<string>();
            this._tDir = 0;
            this._tFile = 0;
            this._Dir = 0;
            this._File = 0;
        }

        public int _Dir;
        public int _File;
        public int _tDir;
        public int _tFile;
        public List<string> _tMessage;
        public List<string> _tError;
    }
}
